<?php

namespace pmvc\data\bind;

use pmvc\util\ReflectionUtil;
use pmvc\data\validation\Validator;
use pmvc\data\validation\ClosureValidator;
use pmvc\data\transform\Transformer;
use pmvc\data\transform\ClosureTransformer;
use pmvc\util\StringUtil;

use Exception;

class ObjectBinder {

	public $transformers 		= Array();
	public $fieldValidators 	= Array();
	public $objectValidators 	= Array();

	/**
	 * Creates a {@link Transformer} if needed.
	 * @param mixed $validator
	 */
	private function asTransformer($transformer) {
		return (is_callable($transformer))
			? new ClosureTransformer($transformer)
			: $transformer;
	}

	/**
	 * Creates a {@link Validator} if needed.
	 * @param mixed $validator
	 */
	private function asValidator($validator) {
		return (is_callable($validator))
			? new ClosureValidator($validator)
			: $validator;
	}

	/**
	 * Sets the {@link Transformer} for a given field.
	 * @param string $fieldName
	 * @param mixed $transformer
	 * @param string $errorMessage
	 * @param bool $skipEmpty whether or not to skip transforming empty values
	 */
	public function setTransformer($fieldName, $transformer, $errorMessage='Invalid format for {fieldName}', $skipEmpty=false) {
		$this->transformers[$fieldName] = Array($this->asTransformer($transformer), $errorMessage, $skipEmpty);
	}

	/**
	 * Sets the {@link Transformer} for the given fields.
	 * @param array $fieldNames
	 * @param mixed $transformer
	 * @param string $errorMessage
	 * @param bool $skipEmpty whether or not to skip transforming empty values
	 */
	public function setTransformerForFields(Array $fieldNames, $transformer, $errorMessage='Invalid format for {fieldName}', $skipEmpty=false) {
		foreach ($fieldNames as $fieldName) {
			$this->setTransformer($fieldName, $this->asTransformer($transformer), $errorMessage, $skipEmpty);
		}
	}

	/**
	 * Adds a {@link Validator} for the given field.
	 * @param string $fieldName
	 * @param mixed $validator
	 * @param string $errorMessage
	 */
	public function addFieldValidator($fieldName, $validator, $errorMessage='Invalid field value for {fieldName}') {
		$this->fieldValidators[$fieldName][] = Array($this->asValidator($validator), $errorMessage);
	}

	/**
	 * Adds a {@link Validator} for the object.
	 * @param mixed $validator
	 * @param string $errorMessage
	 */
	public function addObjectValidator($validator, $errorMessage='Invalid value') {
		$this->objectValidators[] = Array($this->asValidator($validator), $errorMessage);
	}

	/**
	 * Binds the fields to the given object.  If no fields are
	 * given then the fields that already exist on the object
	 * are used.
	 * @param object $object
	 * @param array $fields
	 * @return BindResult the bind result
	 */
	public function bind(&$object, Array $fields=null) {

		// errors
		$bindResult = new BindResult($object);

		// get fields from object if we don't have any
		if (!isset($fields) || empty($fields)) {
			$fields = Array();
			$properties = ReflectionUtil::getProperties($object);
			foreach ($properties as $property) {
				$fields[$property] = ReflectionUtil::getProperty($object, $property);
			}
		}

		// transform and bind each field
		foreach ($fields as $fieldName => $value) {
			$this->transformField($bindResult, $fieldName, $value);
		}

		// now validate each field
		foreach ($fields as $fieldName => $value) {
			$this->validateField($bindResult, $fieldName, $value);
		}

		// lastly, validate the object
		$this->validateObject($bindResult, $object);

		// return the errors
		return $bindResult;
	}

	/**
	 * Transforms the given value for the given field.
	 * @param BindResult $bindResult
	 * @param string $fieldName
	 * @param mixed $value
	 */
	private function transformField(BindResult $bindResult, $fieldName, $value) {

		// get the object
		$object = $bindResult->getObject();

		// transform
		if (isset($this->transformers[$fieldName])) {
			
			// skip nulls
			if (!isset($value) || empty($value) && $this->transformers[$fieldName][2]===true) {
				$value = null;
				
			// transform
			} else {
				try {
					$value = $this->transformers[$fieldName][0]->transform($value);
				} catch (Exception $ex) {
					$bindResult->addFieldError($fieldName, $value, $this->transformers[$fieldName][1]);
					$value = null;
				}
			}
		}
		
		// set the value
		ReflectionUtil::setProperty($object, $fieldName, $value);
	}

	/**
	 * Validates the given value for the given field.
	 * @param array $errors
	 * @param string $fieldName
	 * @param mixed $value
	 */
	private function validateField(BindResult $bindResult, $fieldName, $value) {

		// get validators
		$validators = isset($this->fieldValidators[$fieldName])
			? $this->fieldValidators[$fieldName] : Array();
	
		// run them
		foreach ($validators as $validator) {
			if (!$validator[0]->validate($value)) {
				$bindResult->addFieldError($fieldName, $value, $validator[1]);
			}
		}

	}

	/**
	 * Validates the object.
	 * @param BindResult $bindResult
	 */
	private function validateObject(BindResult $bindResult) {

		// get the object
		$object = $bindResult->getObject();
	
		// run them
		foreach ($this->objectValidators as $validator) {
			if (!$validator[0]->validate($object)) {
				$bindResult->addObjectError($validator[1]);
			}
		}
	}

}